/*
 * Unidata Platform Community Edition
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 * This file is part of the Unidata Platform Community Edition software.
 *
 * Unidata Platform Community Edition is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Unidata Platform Community Edition is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */

package org.unidata.mdm.meta.service.impl.data.instance;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.unidata.mdm.core.context.DataRecordContext;
import org.unidata.mdm.core.type.model.AttributeElement;
import org.unidata.mdm.core.type.model.ComplexElement;
import org.unidata.mdm.core.type.model.GeneratingElement;
import org.unidata.mdm.core.type.model.GenerationStrategyType;
import org.unidata.mdm.core.type.model.IndexingParamsElement;
import org.unidata.mdm.core.type.model.LookupLinkElement;
import org.unidata.mdm.core.type.model.MeasuredValueElement;
import org.unidata.mdm.core.type.model.NamedDisplayableElement;
import org.unidata.mdm.core.type.model.ReferencePresentationElement;
import org.unidata.mdm.core.type.model.instance.AbstractNamedDisplayableCustomPropertiesImpl;
import org.unidata.mdm.core.type.model.instance.AbstractNamedDisplayableImpl;
import org.unidata.mdm.core.type.model.support.AttributeValueGenerator;
import org.unidata.mdm.meta.service.impl.instance.ReferencePresentationImpl;
import org.unidata.mdm.meta.type.model.MetaModelAttribute;
import org.unidata.mdm.meta.type.model.SimpleDataType;
import org.unidata.mdm.meta.type.model.ValueGenerationStrategy;
import org.unidata.mdm.meta.type.model.attributes.AbstractSearchableMetaModelAttribute;
import org.unidata.mdm.meta.type.model.attributes.ArrayMetaModelAttribute;
import org.unidata.mdm.meta.type.model.attributes.AttributeType;
import org.unidata.mdm.meta.type.model.attributes.CodeMetaModelAttribute;
import org.unidata.mdm.meta.type.model.attributes.ComplexMetaModelAttribute;
import org.unidata.mdm.meta.type.model.attributes.SimpleMetaModelAttribute;
import org.unidata.mdm.meta.type.model.attributes.SimpleTypeMetaModelAttribute;
import org.unidata.mdm.meta.type.model.entities.AbstractEntity;
import org.unidata.mdm.meta.type.model.strategy.CustomValueGenerationStrategy;
import org.unidata.mdm.meta.util.ValueGeneratingUtils;
import org.unidata.mdm.search.type.FieldType;
import org.unidata.mdm.search.type.IndexField;

/**
 * @author Mikhail Mikhailov
 */
public class AttributeImpl extends AbstractNamedDisplayableCustomPropertiesImpl
    implements AttributeElement, MeasuredValueElement, ComplexElement, LookupLinkElement, GeneratingElement, IndexField, IndexingParamsElement {
    /**
     * The attribute type.
     */
    private final AttributeType attributeType;
    /**
     * Attribute definition.
     */
    private final MetaModelAttribute attribute;
    /**
     * Entity definition.
     */
    private final NamedDisplayableElement container;
    /**
     * Lookup reference presentation.
     */
    private final ReferencePresentationElement presentation;
    /**
     * Parent link.
     */
    private final AttributeElement parent;
    /**
     * Children.
     */
    private final List<AttributeElement> children = new ArrayList<>();
    /**
     * Calculated path.
     */
    private final String path;
    /**
     * Path broken up in tokens.
     */
    private final String[] tokens;
    /**
     * The depth of this path.
     */
    private final int level;
    /**
     * Order.
     */
    private final int order;
    /**
     * Code alternative flag.
     */
    private final boolean codeAlternative;
    /**
     * Value generator.
     */
    private final AttributeValueGenerator generator;
    /**
     * Constructor.
     * @param attr the attribute
     * @param parent parent link
     * @param path calculated path
     * @param level depth of this attribute
     */
    public AttributeImpl(SimpleMetaModelAttribute attr, AbstractEntity<?> entity, AttributeElement parent, String path, int level) {
        super(attr.getName(), attr.getDisplayName(), attr.getDescription(), attr.getCustomProperties());
        this.attribute = attr;
        this.container = new ContainerInfoImpl(entity);
        this.parent = parent;
        this.path = path;
        this.level = level;
        this.order = attr.getOrder();
        this.attributeType = AttributeType.SIMPLE;
        this.tokens = StringUtils.split(path, '.');
        this.codeAlternative = false;
        this.generator = getGenerator(attribute.getValueGenerationStrategy());
        this.presentation = getCalculatedReferencePresentation();
    }
    /**
     * Constructor.
     * @param attr the attribute
     * @param parent parent link
     * @param path calculated path
     * @param level depth of this attribute
     */
    public AttributeImpl(ArrayMetaModelAttribute attr, AbstractEntity<?> entity, AttributeElement parent, String path, int level) {
        super(attr.getName(), attr.getDisplayName(), attr.getDescription(), attr.getCustomProperties());
        this.attribute = attr;
        this.container = new ContainerInfoImpl(entity);
        this.parent = parent;
        this.path = path;
        this.level = level;
        this.order = attr.getOrder();
        this.attributeType = AttributeType.ARRAY;
        this.tokens = StringUtils.split(path, '.');
        this.codeAlternative = false;
        this.generator = getGenerator(attribute.getValueGenerationStrategy());
        this.presentation = getCalculatedReferencePresentation();
    }
    /**
     * Constructor.
     * @param attr the attribute
     * @param parent parent link
     * @param path calculated path
     * @param level depth of this attribute
     * @param isAlternative tells whether this code attr is an alternative one
     */
    public AttributeImpl(CodeMetaModelAttribute attr, AbstractEntity<?> entity, AttributeElement parent, String path, int level, boolean isAlternative) {
        super(attr.getName(), attr.getDisplayName(), attr.getDescription(), attr.getCustomProperties());
        this.attribute = attr;
        this.container = new ContainerInfoImpl(entity);
        this.parent = parent;
        this.path = path;
        this.level = level;
        this.order = -1;
        this.attributeType = AttributeType.CODE;
        this.tokens = StringUtils.split(path, '.');
        this.codeAlternative = isAlternative;
        this.generator = getGenerator(attribute.getValueGenerationStrategy());
        this.presentation = getCalculatedReferencePresentation();

    }
    /**
     * Constructor.
     * @param attr the attribute
     * @param parent parent link
     * @param path calculated path
     * @param level depth of this attribute
     */
    public AttributeImpl(ComplexMetaModelAttribute attr, AbstractEntity<?> entity, AttributeElement parent, String path, int level) {
        super(attr.getName(), attr.getDisplayName(), attr.getDescription(), attr.getCustomProperties());
        this.attribute = attr;
        this.container = new ContainerInfoImpl(entity);
        this.parent = parent;
        this.path = path;
        this.level = level;
        this.order = attr.getOrder();
        this.attributeType = AttributeType.COMPLEX;
        this.tokens = StringUtils.split(path, '.');
        this.codeAlternative = false;
        this.generator = getGenerator(attribute.getValueGenerationStrategy());
        this.presentation = getCalculatedReferencePresentation();
    }
    /**
     * @return the attribute
     */
    public MetaModelAttribute getAttribute() {
        return attribute;
    }
    @Override
    public boolean isMeasured() {
        return isSimple() && ((SimpleMetaModelAttribute) attribute).getSimpleDataType() == SimpleDataType.MEASURED;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public MeasuredValueElement getMeasured() {
        return isMeasured() ? this : null;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public String getCategoryId() {
        return Objects.isNull(((SimpleMetaModelAttribute) attribute).getMeasureSettings())
                ? null
                : ((SimpleMetaModelAttribute) attribute).getMeasureSettings().getCategoryId();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public String getDefaultUnitId() {
        return Objects.isNull(((SimpleMetaModelAttribute) attribute).getMeasureSettings())
                ? null
                : ((SimpleMetaModelAttribute) attribute).getMeasureSettings().getDefaultUnitId();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isIndexed() {
        // Nothing is checked for now.
        // 'searchable' and 'hidden' flags can be checked in the future
        return isComplex() || getValueType().toSearchType() != null;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public IndexField getIndexed() {
        return isIndexed() ? this : null;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isAnalyzed() {
        return getValueType() == AttributeValueType.STRING
                && !isClob()
                && !isBlob()
                && !isEnumValue()
                && !isLinkTemplate()
                && !isDictionary()
                && !isCode()
                && !isCodeAlternative()
                && !isLookupLink();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean hasIndexingParams() {
        if (isCode() || isSimple() || isArray()) {
            return ((AbstractSearchableMetaModelAttribute<?>) attribute).isSearchable();
        }
        return false;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public IndexingParamsElement getIndexingParams() {
        return hasIndexingParams() ? this : null;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isMorphological() {
        if (isArray()) {
            return ((ArrayMetaModelAttribute) attribute).isSearchMorphologically();
        } else if (isSimple()) {
            return ((SimpleMetaModelAttribute) attribute).isSearchMorphologically();
        }
        return false;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isCaseInsensitive() {
        if (isArray()) {
            return ((ArrayMetaModelAttribute) attribute).isSearchCaseInsensitive();
        } else if (isSimple()) {
            return ((SimpleMetaModelAttribute) attribute).isSearchCaseInsensitive();
        }
        return false;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isGenerating() {
        return Objects.nonNull(generator);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public GeneratingElement getGenerating() {
        return isGenerating() ? this : null;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Object generate(DataRecordContext input) {
        return generator.generate(this, input);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public GenerationStrategyType getStrategyType() {

        if (isGenerating()) {

            switch (attribute.getValueGenerationStrategy().getStrategyType()) {
                case CONCAT:
                    return GenerationStrategyType.CONCAT;
                case CUSTOM:
                    return GenerationStrategyType.CUSTOM;
                case RANDOM:
                    return GenerationStrategyType.RANDOM;
                case SEQUENCE:
                    return GenerationStrategyType.SEQUENCE;
            }
        }

        return null;
    }
    @Override
    public boolean isComplex() {
        return attributeType == AttributeType.COMPLEX;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public ComplexElement getComplex() {
        return isComplex() ? this : null;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public int getMinCount() {
        final Integer minCount = ((ComplexMetaModelAttribute) attribute).getMinCount();
        return minCount != null ? minCount : 0;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public int getMaxCount() {
        final Integer maxCount = ((ComplexMetaModelAttribute) attribute).getMaxCount();
        return maxCount != null ? maxCount.intValue() : Integer.MAX_VALUE;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public String getNestedEntityName() {
        return ((ComplexMetaModelAttribute) attribute).getNestedEntityName();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isLookupLink() {
        return (isSimple() && StringUtils.isNotBlank(((SimpleMetaModelAttribute) attribute).getLookupEntityType()))
            || (isArray() && StringUtils.isNotBlank(((ArrayMetaModelAttribute) attribute).getLookupEntityType()));
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public LookupLinkElement getLookupLink() {
        return isLookupLink() ? this : null;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public ReferencePresentationElement getPresentation() {
        return presentation;
    }
    /**
     * Gets lookup link name.
     * @return the name
     */
    @Override
    public String getLookupLinkName() {

        if (isSimple()) {
            return ((SimpleMetaModelAttribute) attribute).getLookupEntityType();
        } else if (isArray()) {
            return ((ArrayMetaModelAttribute) attribute).getLookupEntityType();
        }

        return null;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public String getExchangeSeparator() {

        if (isArray()) {
            return ((ArrayMetaModelAttribute) attribute).getExchangeSeparator();
        }

        return null;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isNullable() {

        if (isSimple()) {
            return ((SimpleMetaModelAttribute) attribute).isNullable();
        } else if (isArray()) {
            return ((ArrayMetaModelAttribute) attribute).isNullable();
        } else if (isCode()) {
            return ((CodeMetaModelAttribute) attribute).isNullable();
        }

        return false;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public AttributeValueType getValueType() {

        // Cannot calculate it statically,
        // due to delayed value type calculation for lookup links
        if (isComplex()) {
            return AttributeValueType.NONE;
        } else if (isEnumValue() || isLinkTemplate() || isDictionary()) {
            return AttributeValueType.STRING;
        }

        SimpleDataType t = isLookupLink()
                ? getLookupEntityCodeAttributeType()
                : getSimpleDataType();

        if (t == null) {
            return AttributeValueType.NONE;
        }

        switch (t) {
        case ANY:
            return AttributeValueType.ANY;
        case BLOB:
            return AttributeValueType.BLOB;
        case BOOLEAN:
            return AttributeValueType.BOOLEAN;
        case CLOB:
            return AttributeValueType.CLOB;
        case DATE:
            return AttributeValueType.DATE;
        case INTEGER:
            return AttributeValueType.INTEGER;
        case MEASURED:
            return AttributeValueType.MEASURED;
        case NUMBER:
            return AttributeValueType.NUMBER;
        case STRING:
            return AttributeValueType.STRING;
        case TIME:
            return AttributeValueType.TIME;
        case TIMESTAMP:
            return AttributeValueType.TIMESTAMP;
        default:
            break;
        }

        return null;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public String getEnumName() {
        return isEnumValue() ? ((SimpleMetaModelAttribute) attribute).getEnumDataType() : null;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public String getLinkTemplate() {
        return isLinkTemplate() ? ((SimpleMetaModelAttribute) attribute).getLinkDataType() : null;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public String getPath() {
        return path;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public FieldType getFieldType() {
        return isComplex() ? FieldType.COMPOSITE : getValueType().toSearchType();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public NamedDisplayableElement getContainer() {
        return container;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public AttributeElement getParent() {
        return parent;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public List<AttributeElement> getChildren() {
        return children;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean hasParent() {
        return parent != null;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean hasChildren() {
        return !children.isEmpty();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public String getTypeName() {
        return attributeType.name();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public int getLevel() {
        return level;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public int getOrder() {
        return order;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isSimple() {
        return attributeType == AttributeType.SIMPLE;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isCode() {
        return attributeType == AttributeType.CODE;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isCodeAlternative() {
        return codeAlternative;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isArray() {
        return attributeType == AttributeType.ARRAY;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isLinkTemplate() {
        return isSimple() && StringUtils.isNotBlank(((SimpleMetaModelAttribute) attribute).getLinkDataType());
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isEnumValue() {
        return isSimple() && StringUtils.isNotBlank(((SimpleMetaModelAttribute) attribute).getEnumDataType());
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isDictionary() {
        if (isSimple()) {
            return CollectionUtils.isNotEmpty(((SimpleMetaModelAttribute) attribute).getDictionaryDataType());
        } else if (isArray()){
            return CollectionUtils.isNotEmpty(((ArrayMetaModelAttribute) attribute).getDictionaryDataType());
        }
        return false;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isBlob() {
        return isSimple() && ((SimpleMetaModelAttribute) attribute).getSimpleDataType() == SimpleDataType.BLOB;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isClob() {
        return isSimple() && ((SimpleMetaModelAttribute) attribute).getSimpleDataType() == SimpleDataType.CLOB;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isDate() {
        return isSimple() && (
                ((SimpleMetaModelAttribute) attribute).getSimpleDataType() == SimpleDataType.DATE ||
                ((SimpleMetaModelAttribute) attribute).getSimpleDataType() == SimpleDataType.TIME ||
                ((SimpleMetaModelAttribute) attribute).getSimpleDataType() == SimpleDataType.TIMESTAMP
        );
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isUnique() {
        return isCode() || (isSimple() && ((SimpleMetaModelAttribute) attribute).isUnique());
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isDisplayable() {
        if (isCode() || isSimple() || isArray()) {
            return ((AbstractSearchableMetaModelAttribute<?>) attribute).isDisplayable();
        }
        return false;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isMainDisplayable() {

        if (isCode() || isSimple()) {
            return ((SimpleTypeMetaModelAttribute) attribute).isMainDisplayable();
        } else if (isArray()) {
            return ((ArrayMetaModelAttribute) attribute).isMainDisplayable();
        }

        return false;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isReadOnly() {
        return attribute.isReadOnly();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isHidden() {
        return attribute.isHidden();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isOfPath(String path) {

        if (StringUtils.length(path) == 0 && level == 0) {
            return true;
        }

        String[] parts = StringUtils.split(path, '.');
        if (level != parts.length) {
            return false;
        }

        for (int i = level - 1; i >= 0; i--) {
            if (!StringUtils.equals(tokens[i], parts[i])) {
                return false;
            }
        }

        return true;
    }
    /**
     * @return the SimpleDataType of attribute or null
     */
    private SimpleDataType getSimpleDataType() {

        if (isSimple()) {
            return ((SimpleMetaModelAttribute) attribute).getSimpleDataType();
        } else if (isArray()) {
            return ((ArrayMetaModelAttribute) attribute).getArrayValueType().value();
        } else if (isCode()) {
            return ((CodeMetaModelAttribute) attribute).getSimpleDataType();
        }

        return null;
    }
    /**
     * @return the SimpleDataType of attribute or null
     */
    private SimpleDataType getLookupEntityCodeAttributeType() {

        if (isLookupLink()) {

            if (isSimple()) {
                return ((SimpleMetaModelAttribute) attribute).getLookupEntityCodeAttributeType();
            }

            if (isArray()) {
                return ((ArrayMetaModelAttribute) attribute).getLookupEntityCodeAttributeType().value();
            }
        }

        return null;
    }

    private AttributeValueGenerator getGenerator(ValueGenerationStrategy s) {

        if (Objects.nonNull(s)) {
            switch (s.getStrategyType()) {
            case CONCAT:
                return ValueGeneratingUtils.CONCAT_ATTRIBUTE_GENERATOR;
            case RANDOM:
                return ValueGeneratingUtils.RANDOM_ATTRIBUTE_GENERATOR;
            case CUSTOM:
                return ValueGeneratingUtils.defineAttributeCustomValueGenerator(((CustomValueGenerationStrategy) s).getClassName());
            default:
                break;
            }
        }

        return null;
    }

    private ReferencePresentationElement getCalculatedReferencePresentation() {

        if (!isLookupLink()) {
            return null;
        }

        Collection<String> displayAttributes;
        Collection<String> searchAttributes;
        boolean showNames;

        if (isSimple()) {
            displayAttributes = ((SimpleMetaModelAttribute) attribute).getLookupEntityDisplayAttributes();
            searchAttributes = ((SimpleMetaModelAttribute) attribute).getLookupEntitySearchAttributes();
            showNames = (((SimpleMetaModelAttribute) attribute).isUseAttributeNameForDisplay());
        } else if (isArray()) {
            displayAttributes = ((ArrayMetaModelAttribute) attribute).getLookupEntityDisplayAttributes();
            searchAttributes = ((ArrayMetaModelAttribute) attribute).getLookupEntitySearchAttributes();
            showNames = (((ArrayMetaModelAttribute) attribute).isUseAttributeNameForDisplay());
        } else {
            return null;
        }

        return new ReferencePresentationImpl(displayAttributes, searchAttributes, showNames);
    }
    /**
     * @author Mikhail Mikhailov
     * Short top container digest.
     */
    private class ContainerInfoImpl extends AbstractNamedDisplayableImpl {
        /**
         * Constructor.
         * @param ae the entity
         */
        private ContainerInfoImpl(AbstractEntity<?> ae) {
            super(ae.getName(), ae.getDisplayName(), ae.getDescription());
        }
    }
}
